#!/usr/bin/env ruby

LKP_SRC = ENV['LKP_SRC'] || File.dirname(File.dirname(File.dirname(File.realpath($PROGRAM_NAME))))

require 'ostruct'
require "#{LKP_SRC}/lib/statistics"
require "#{LKP_SRC}/lib/string"
require "#{LKP_SRC}/lib/array"
require "#{LKP_SRC}/lib/tests/stats"
require "#{LKP_SRC}/lib/assert"

# # selftests: vDSO: vdso_test_correctness    <= level 0
# # [RUN] Testing clock_gettime for clock...  <= level 1
# # [OK]  Test Passed.

# # selftests: mm: run_vmtests.sh             <= level 0
# # -----------------------
# # running ./madv_populate                   <= level 1
# # -----------------------
# # # TAP version 13
# # 1..21
# # # [RUN] test_prot_read                    <= level 1
# # ok 1 MADV_POPULATE_READ with PROT_READ    <= level 1

# # selftests: mm: run_vmtests.sh             <= level 0
# # running ./memfd_secret                    <= level 1
# # ----------------------
# # page_size: 4096, mlock.soft: 8388608
# # TAP version 13
# # 1..4
# # ok 2 # SKIP memfd_secret is not supported <= level 1

# # selftests: mm: madv_populate              <= level 0
# # TAP version 13
# # 1..21
# # # [RUN] test_prot_read                    <= level 1
# # ok 1 MADV_POPULATE_READ with PROT_READ    <= level 1

# # selftests: sgx: test_sgx                  <= level 0
# # TAP version 13
# # #  RUN           enclave.unclobbered      <= level 1
# # ok 3 # SKIP System does not support SGX2  <= level 1

# # selftests: mm: soft-dirty                 <= level 0
# # TAP version 13
# # 1..5
# # ok 1 Test test_simple                     <= level 1

# # selftests: cgroup: test_memcontrol        <= level 0
# # not ok 7 test_memcg_max                   <= level 1
# # ok 10 # SKIP test_memcg_swap_max          <= level 1
class Stater
  # ok 1 selftests: clone3: clone3
  # ok 18 selftests: kvm: kvm_create_max_vcpus
  # not ok 2 selftests: pstore: pstore_post_reboot_tests # SKIP
  LEVEL_0_TEST_RESULT_PATTERN = /^(?<result>ok|not ok).*selftests: (?<test_parts>.+)/.freeze

  # # ok 6 [1246] Result (-22) matches expectation (-22)
  # # ok 3 # SKIP System does not support SGX2
  # # ok 117 # SKIP write_invalid.NVidia.11
  # # ok 11 enclave.augment_via_eaccept # SKIP SGX2 not supported
  # # ok 2 MADV_POPULATE_WRITE with PROT_READ
  # # ok 1 [RUN]	test_alloc_timeline
  LEVEL_N_TEST_RESULT_PATTERN = /^(?:# )+(?<result>ok|not ok) \d+ (?<test_parts>.+)/.freeze

  attr_reader :stats, :test_level

  def initialize(test_dir, test_script, stats)
    @test_dir = test_dir
    @test_script = test_script

    @test_level = 0

    @tests = Hash.new { |h, k| h[k] = OpenStruct.new(parts: []) }
    curr_test.parts = [[@test_dir, @test_script].compact.join('.')]

    @stats = stats
  end

  def stat(line)
    case line
    when /TAP version 13/
      @test_level = count_leading_hashes(line)

      # The test will start running only after the 'make' is successful.
      # During this process, this 'TAP' line will be outputted.
      parse_make_result('pass')
    when /^# selftests: (.+): (.+)/
      assert test_level.zero?, "logic error: test_level #{test_level} not equal 0"

      @test_level = 1
    when LEVEL_0_TEST_RESULT_PATTERN
      @test_level = 0

      parse_tap_0_test_result(Regexp.last_match)
    when LEVEL_N_TEST_RESULT_PATTERN
      @test_level = count_leading_hashes(line)

      parse_test_result(Regexp.last_match)
    when /^(?:# )+(?<test_parts>.*)\.\.\.\s*\[?(?<result>ok|OK|not ok|pass|PASS|fail|skip|SKIPPING)\]?$/, # TEST: multiple livepatches ... ok
         /^(?:# )+(?<test_parts>.*)\s*\[ ?(?<result>OK|PASS|FAIL|SKIP|UNSUPPORTED) ?\]$/, # Estimating clock drift: 0.0(est) 0.0(act)     [OK]
         /^(?:# )+(?<test_parts>.*):\s*(?<result>OK|PASS|FAIL|SKIP)$/, # Queue open with mq_maxmsg > limit when euid = 0 succeeded:            PASS
         /^(?<result>not ok|ok|fail|skip)\s\d+\s(?<test_parts>.*)/,
         /^(?:# )+(?<result>OK|PASS|FAIL|SKIP): (?<test_parts>.+)/, # # PASS: /dev/mem 0x0-0xa0000 is readable
         /^#  (?<test_parts>.*)\s+(?<result>ok|fail)$/
      parse_test_result(Regexp.last_match)
    when /^# Running test: (\w+_\d+) - run #(\d+)/, # # Running test: kmod_test_0005 - run #1
         /^(?:# )+\s*\[?RUN\]?\s(.+)/, # # # [RUN] test_prot_read
         /^# Running (.+)/, # # Running kernel configuration test 1 -- rare
         /^# # Testing (.+):/ # # # Testing allocation and importing:
      curr_test.parts = [test_name($1, $2)]
    when /: recipe for target.+failed$/,
         /^make: \*\*\* (.*) (Error \d+|Stop\.)$/
      # Makefile:47: recipe for target 'sysret_ss_attrs_64' failed
      # make: *** No rule to make target .*, needed by 'all'.  Stop.
      parse_make_result('fail')
    when %r{make: Leaving directory .*/(.*)'}
      @test_dir = @test_script = nil

      @test_level = 0
      @tests.clear
    end
  end

  def curr_test
    @tests[test_level]
  end

  # rli9 FIXME is uniq a must
  def test_name(*parts)
    parts.compact.map(&:strip).reject(&:empty?).uniq.join('.')
  end

  # # ok 3 # SKIP System does not support SGX2
  def count_leading_hashes(line)
    line.scan(/^(?:# )+/).first.to_s.scan(/# /).size
  end

  def parse_make_result(result)
    stats.add test_name(@test_dir, 'make'), result, overwrite: true
  end

  def parse_tap_0_test_result(m)
    test_name = test_name m[:test_parts].sub(': ', '.').sub(/# .+$/, '')

    add_test_result(m, test_name)
  end

  def parse_test_result(m)
    test_parts = Array(curr_test.parts) + Array(m[:test_parts].split(/# SKIP/))
    test_parts = fixup_test_parts(test_parts)

    test_name = test_name(test_prefix, *test_parts)

    add_test_result(m, test_name)
  end

  def fixup_test_parts(test_parts)
    # Testing clock_gettime for clock CLOCK_REALTIME (0)...
    test_parts.compact.map { |part| part.sub(/(^| )TEST: /, '').sub(/\.+$/, '') }
  end

  # leading test name of current test level
  def test_prefix
    parts = @tests.values.first(test_level).map(&:parts).flatten
    test_name(*parts)
  end

  private

  def add_test_result(m, test_name)
    test_result = m[:test_parts] =~ /# SKIP/ ? 'skip' : m[:result]

    # # # [RUN] test_softdirty
    # # ok 17 range is not softdirty
    # # ok 19 range is not softdirty
    #
    # ok 1 selftests: lkdtm: PANIC.sh # SKIP
    # ok 1 selftests: lkdtm: PANIC.sh # SKIP
    stats.add test_name, test_result, overwrite: true
  end
end

class SigaltstackStater < Stater
  def stat(line)
    case line
    when /^# # \[RUN\]\s(.*)/,
         /^# # \[(OK|ok|PASS)\]/
      # ignore
    else
      super
    end
  end
end

class BreakpointsStater < Stater
  def stat(line)
    case line
    when /^# (ok|not ok) \d+ CPU \d+/
      # # ok 1 CPU 0
      # # not ok 3 CPU 2
      # ignore
    else
      super
    end
  end
end

class NetStater < Stater
  def stat(line)
    case line
    when /^# \[\s*(?<result>OK|FAIL|SKIP)\s*\]\s+(?<test_parts>.*)/, # # [OK]   !(flowlabel_get(fd, 1, 255, 0))
         /^# (?<test_parts>.*)(?<result>OK|ok|PASS)$/
      parse_test_result(Regexp.last_match)
    when /^# TEST SECTION: (.*)/,
        /^# ((Control|Data) path: .+)/, # # Control path: Large scale MDB dump - IPv6 overlay / IPv6 underlay
         /^# (ipv\d.*)$/i, # # ipv4
         /^# (.*qdisc on VRF device)/,
         /^# (dgram.*|raw.*)$/,
         /#     (.*device.* down.*)/,
         /#     (.*arrier)/,
         /(OUTPUT tests|INPUT tests|GLOBAL tests)/,
         /^# (?:running|run) ([^\s]+) test/,
         /^# ((Single|Multipath|Single|Admin|Local|Single|FIB|IPv4|IPv6|Basic|Legacy|Routing) (.*))/,
         /(Use cases)/,
         /(Run time tests - ipv4)/
      curr_test.parts = [$1]
    when /^# TEST SUBSECTION: (.*)/
      curr_test.parts << $1
    when /^# (No VRF|With VRF|TCP reset|ICMP unreachable|Device enslaved to bridge|Ping LLA with multiple interfaces)/,
         /#  (\w.*)object file doesn't contain sec xdp_dummy/ # #  no GRO                                  object file doesn't contain sec xdp_dummy
      curr_test.parts[1] = $1
    when /^# SYSCTL: (.*)/
      curr_test.parts[2] = $1
    else
      super
    end
  end

  def fixup_test_parts(test_parts)
    test_parts = Array(test_parts).map do |part|
      case part
      when 'fdb get tests: iproute2 too old'
        # selftests: net: netdevice.sh
        # SKIP: eth0: interface already up

        # below rtnetlink.sh's level_1_test will repeate twice thus cause duplication
        # SKIP: fdb get tests: iproute2 too old
        #
        # If it's passed stat will be
        # PASS: bridge fdb get
        'bridge_fdb_get'
      when /set ifalias \S+ for test-dummy0/
        # set ifalias 82e318f8-b9c2-490c-b15d-28a5a51f8f67 for test-dummy0
        # set ifalias 6099b132-422e-4720-a085-0dc64832052a for test-dummy0
        'set ifalias for test-dummy0'
      when /no TLS support/ # ["", " no TLS support"]
        nil
      when /ns\d+-\w+/
        # ns1-Ip028vuy connection on port 2121 has ftp helper attached
        # ns1-IpVmXxqi can reach ns2-IpVmXxqi
        part.gsub(/(ns\d+)-\w+/, ' \1')
      when /(.+)\s+\d+s/ # nft_concat_range.sh
        # "  net,port                              8s                              "
        part.sub(/\d+s/, '')
      else
        part
      end
    end

    super(test_parts)
  end
end

class NetVethStater < NetStater
  def stat(line)
    case line
    when /^# .+ (ok|fail)\b/
      #         - peer gro flag                                      ok
      # ignore
    else
      super
    end
  end
end

class NetUdpgsoStater < NetStater
  def stat(line)
    case line
    when /^# (ipv\d [^:]+)$/i
      curr_test.parts = [$1]
    when /^# (ipv\d)/i
      # ignore
    else
      super
    end
  end
end

class VdsoStater < Stater
  def stat(line)
    case line
    when /^# (?<test_parts>.*)\[(?<result>OK|FAIL|SKIP)\]\s/
      # [OK]  Test Passed.
      parse_test_result(Regexp.last_match)
    else
      super
    end
  end
end

class MmStater < Stater
  def stat(line)
    case line
    when /# running:?\s+(.+)/
      # running hugepage-shm
      # running ./va_128TBswitch
      # running: gup_test -u # get_user_pages_fast() benchmark
      curr_test.parts = [$1.sub(/^\.\//, '')]
    when /^(?:# )+(?<test_parts>.*)\[(?<result>PASS|FAIL|SKIP)\]/
      parse_test_result(Regexp.last_match)

      curr_test.parts.clear
    when /^# (?<test_parts>[a-zA-Z].*): [0-9a-z]+ - (?<result>OK|FAIL)/
      # mmap(HIGH_ADDR, MAP_FIXED): 0xffffffffffffffff - FAILED
      # mmap(ADDR_SWITCH_HINT - PAGE_SIZE, PAGE_SIZE): 0x7fe8ef2a4000 - OK
      # mmap(ADDR_SWITCH_HINT - PAGE_SIZE, PAGE_SIZE): 0x7fe8ef29d000 - OK
      parse_test_result(Regexp.last_match)
    else
      super
    end
  end
end

class MountStater < Stater
  def stat(line)
    case line
    when /^WARN: No \/proc\/self\/uid_map exist, test skipped/
      # WARN: No /proc/self/uid_map exist, test skipped.
      stats.add @test_dir, 'skip'
    when /(^(MS.+|Default.+) malfunctions$)|(^Mount flags unexpectedly changed after remount$)/
      # Mount flags unexpectedly changed after remount
      stats.add @test_dir, 'fail'
    else
      super
    end
  end
end

class FutexStater < Stater
  def stat(line)
    case line
    when /Arguments: (.+)/
      # # futex_requeue_pi: Test requeue functionality
      # # 	Arguments: broadcast=0 locked=0 owner=0 timeout=0ns
      # ok 1 futex-requeue-pi
      curr_test.parts = [$1]
    else
      super

      curr_test.parts.clear if line =~ LEVEL_N_TEST_RESULT_PATTERN
    end
  end
end

class MptcpStater < Stater
  def stat(line)
    case line
    when /^#\s+(?<result>ok|not ok) \d+ - (?<test_parts>.*)/
      parse_test_result(Regexp.last_match)
    when /\[\s*(OK|FAIL|SKIP)\s*\]$/i
      # 03 ns1 MPTCP -> ns1 (10.0.1.1:10000      ) MPTCP     (duration    46ms) [ OK ]
      # ignore
    else
      super
    end
  end

  def fixup_test_parts(test_parts)
    test_parts = Array(test_parts).map do |part|
      # mptcp_connect: loopback v4: ns1 MPTCP -> ns1 (10.0.1.1:10000      ) MPTCP # time=46ms
      part.sub(/ # time=\d+ms$/, '')
    end

    super(test_parts)
  end
end

class TimensStater < Stater
  def stat(line)
    case line
    when /# SKIP CLOCK_BOOTTIME_ALARM isn't supported/ # rubocop:disable Lint/EmptyWhen
      # ignore below skip, cause there's no test name when it skipped,
      # ok 3 # SKIP CLOCK_BOOTTIME_ALARM isn't supported
      # ok 4 # SKIP CLOCK_BOOTTIME_ALARM isn't supported
    else
      super
    end
  end

  def fixup_test_parts(test_parts)
    # ignore the 'Passed' part
    # # ok 2 Passed for CLOCK_BOOTTIME (vdso)
    super(test_parts.map { |part| part.sub(/^Passed/, '') })
  end
end

class DmaStater < Stater
  def stat(line)
    case line
    when /^# average (map|unmap) latency\(us\):(.*) standard deviation:(.*)/
      # average unmap latency(us):0.6 standard deviation:1.1
      # average map latency(us):0.8 standard deviation:1.2
      stats.add test_name(test_prefix, "average_#{$1}_latency"), $2.to_f
      stats.add test_name(test_prefix, "average_#{$1}_latency_stddev"), $3.to_f
    else
      super
    end
  end
end

class PidfdStater < Stater
  def fixup_test_parts(test_parts)
    # ignore the 'Passed' part
    # # ok 2 pidfd check fdinfo for dead process test: Passed
    # # ok 1 pidfd poll test: pass
    super(test_parts.map { |part| part.sub(/: (pass(ed)?|Skipping test)$/i, '') })
  end
end

class SgxStater < Stater
  def fixup_test_parts(test_parts)
    # ignore the 2nd part which is SKIP reason if exists
    #   ["", "System does not support SGX2"]
    super [test_parts.first]
  end
end

class CapabilitiesStater < Stater
  def stat(line)
    case line
    when /^# # \[RUN\].*(Tests with uid .*) +++/
      # # # [RUN] +++ Tests with uid != 0 +++
      curr_test.parts = [$1]
    when /^# # \[RUN\]\s+(.+)/
      # # # [RUN]       Root => ep
      curr_test.parts[1] = $1
    else
      super
    end
  end

  def fixup_test_parts(test_parts)
    # ignore the 'Passed'
    super(test_parts.reject { |part| part =~ /Passed/ })
  end
end

class KmodStater < Stater
  def stat(line)
    case line
    when /^# (?<test_parts>.*)(?:kmod_test_.+|kmod_check_visibility): (?<result>OK|FAIL|SKIP)/
      parse_test_result(Regexp.last_match)
    else
      super
    end
  end
end

class NetfilterStater < NetStater
  def stat(line)
    case line
    when /^# TEST: (.+)/ # # TEST: reported issues
      curr_test.parts = [$1]
    else
      super
    end
  end
end

class MqueueStater < Stater
  attr_accessor :mqueue_speed

  def initialize(test, test_script, stats)
    super(test, test_script, stats)

    @mqueue_speed = {}
  end

  def stat(line)
    case line
    when /Test #([1-9].*):/
      # Test #2b: Time send/recv message, queue full, increasing prio
      @mqueue_test = Regexp.last_match[1]
    when /(Send|Recv) msg:/
      #  Send msg:                       0.48443412s total time
      @io = Regexp.last_match[1]
    when %r{(\d+) nsec/msg}
      # 484 nsec/msg
      @mqueue_speed["#{@mqueue_test}.#{@io}"] = Regexp.last_match[1].to_i
    when /make: Leaving.*mqueue'/
      stats.add test_name(@test_dir, 'nsec_per_msg'), @mqueue_speed.values.average.to_i unless @mqueue_speed.empty?
    else
      super
    end
  end
end

class BpfStater < Stater
  def stat(line)
    case line
    when /^# (?<result>fail|Pass):\s(?<test_parts>.*)/, # # fail: xdpgeneric arp(F_BROADCAST) ns1-1
         /^# Test case:\s(?<test_parts>.*)\s\.\.\s\[(?<result>PASS|FAIL)\]/, # Test case: sysctl wrong attach_type .. [PASS]
         /^(?:#| ) #(?:\d+|\d+\/\d+)\s+(?<test_parts>[^:]+):(?<result>OK|FAIL|SKIP)/, # #1/11 variable subtraction:OK
         /^# selftests:\s(?<test_parts>.*)\s\[(?<result>PASS|SKIP|FAIL)/, # selftests: test_xdp_redirect $xdpmode [FAILED]
         /^# main:(?<result>PASS|FAIL):(?<test_parts>.*)/, # main:PASS:cgroup_setup_and_join
         /^# #\d+\/(?<test_parts>[up]\s.*)\s(?<result>SKIP|OK)/, # #0/u invalid and of negative number SKIP
         /^# (?<test_parts>.*)(?<result>PASS|FAIL|Pass)$/
      parse_test_result(Regexp.last_match)
    when /^# starting ((?:egress|ingress)\s.*)/
      curr_test.parts = [$1]
    else
      # not ok 6 selftests: bpf: test_progs # exit=1
      super
    end
  end

  def fixup_test_parts(test_parts)
    # "Test   0: mov ... "
    test_parts = Array(test_parts).map { |part| part.sub(' ...', '') }

    super(test_parts)
  end
end

class VmallocStater < Stater
  attr_accessor :tmp_stats

  def initialize(test, test_script, stats)
    super(test, test_script, stats)

    @tmp_stats = {}
  end

  def stat(line)
    case line
    when /(Check the kernel ring buffer to see the summary|Check the kernel message buffer to see the summary)/
      stats.add @test_dir, 'pass'
    when /Summary: (.+) passed: (.+) failed: (.+) repeat: (.+) loops: (.+) avg: (.+) usec/
      # vmalloc test stat: (stress test worker number = nr_threads)
      # [  223.093027] Summary: fix_size_alloc_test passed: 1 failed: 0 repeat: 1 loops: 1000000 avg: 5861476 usec
      # [  223.176299] Summary: kvfree_rcu_1_arg_vmalloc_test passed: 1 failed: 0 repeat: 1 loops: 1000000 avg: 6248522 usec
      # [  223.187358] Summary: kvfree_rcu_2_arg_vmalloc_test passed: 1 failed: 0 repeat: 1 loops: 1000000 avg: 6333619 usec
      # [  223.198414] All test took worker0=441656368064 cycles
      result = 'fail'
      if $3.to_i.zero?
        @tmp_stats["#{$1}.usec_per_loop"] = $6.to_i
        result = 'pass'
      end

      @tmp_stats[$1] = result
    when /All test took (.+)=(.+) cycles/
      raise "unexpected summary: #{@test_dir}: #{line}" if @tmp_stats.empty?

      @tmp_stats.each do |key, value|
        stats.add test_name(@test_dir, $1, key), value
      end
      @tmp_stats = {}
    else
      super
    end
  end
end

class SyncStater < Stater
  def fixup_test_parts(test_parts)
    super(test_parts.map { |part| part.sub(/\[RUN\]\s+/, '') })
  end
end

stats = LKP::Stats.new

while (line = $stdin.gets)
  line = line.resolve_invalid_bytes.chomp

  case line
  when /^# selftests: net: (udpgso|veth)\.sh/
    test_script = $1
    stater = Object.const_get("Net#{test_script.split(/[-_]/).map(&:capitalize).join}Stater")
                   .new('net', "#{test_script}.sh", stats)
  when /^# selftests: (net\/(mptcp|netfilter)): (.*\.sh)/
    # selftests: net/mptcp: mptcp_connect.sh
    stater = Object.const_get("#{$2.split('-').map(&:capitalize).join}Stater")
                   .new($1, $3, stats)
  when /^# selftests: (.+): (.+)/,
       /make: Entering directory .*\/(.*)'/
    # selftests: kmod: kmod.sh
    stater_cls = begin
      Object.const_get("#{$1.split('-').map(&:capitalize).join}Stater")
    rescue NameError
      Stater
    end

    stater = stater_cls.new $1, $2, stats
  when /mm\/test_vmalloc.sh (stress|performance)/
    stater = VmallocStater.new("mm.test_vmalloc.sh.#{$1}", nil, stats)
  else
    next unless stater
  end

  stater.stat(line)
end

stats.dump('ok' => 'pass', 'not_ok' => 'fail', 'skipping' => 'skip')
